#include "GLTFParser.h"
#include "platform/PlatUtil.h"
#include "core/CUtil.h"
#include "OImage.h"
#include "DiskCache.h"

CMap<ModelType, int> GLTFParser::s_meshNumMap;
CMap<ModelType, CMap<int, CString>> GLTFParser::s_meshNames;
CMap<ModelType, CMap<int, CVector<glm::vec3>>> GLTFParser::s_vertices;
CMap<ModelType, CMap<int, CVector<UInt>>> GLTFParser::s_indices;
CMap<ModelType, CMap<int, CVector<glm::vec2>>> GLTFParser::s_texcoords;
CMap<ModelType, CMap<int, CVector<glm::vec3>>> GLTFParser::s_normals;
CMap<ModelType, CMap<int, glm::mat4>> GLTFParser::s_transforms;
CMap<ModelType, CMap<int, PbrMaterial>> GLTFParser::s_materials;
CMap<ModelType, GltfInfo> GLTFParser::s_gltfInfoMap;

GltfInfo GLTFParser::s_zhicheInfo;
bool GLTFParser::s_isZhicheParsedDone = false;

/**@note use `thread_local` to avoid different thread(zhiche's parsing) using shared global variables!*/
thread_local ModelType type;
thread_local int meshIndex;
thread_local float minX, minY, minZ, maxX, maxY, maxZ;
void resetRuler(ModelType _type) {
	type = _type;
	meshIndex = 0;
	minX = minY = minZ = std::numeric_limits<float>::max();
	maxX = maxY = maxZ = std::numeric_limits<float>::lowest();/**@note not min()! */
}

void GLTFParser::parseObj(ModelType type) {
	tinygltf::Model model;
	tinygltf::TinyGLTF loader;
	std::string err;
	std::string warn;

	CByteArray bytes;
	PlatUtil::readFileAsBytes(bytes, Config::modelPath(type));

	bool ret = loader.LoadBinaryFromMemory(&model, &err, &warn, bytes.data(), bytes.size());

	if (!warn.empty()) LogW << warn;
	if (!err.empty()) LogE << err;
	if (!ret) {
		LogE << "modelType = " << static_cast<int>(type) << " load error!";
		throw std::runtime_error("Failed to load glTF model!");
	}

	resetRuler(type);

	for (const auto& scene : model.scenes) {
		for (const auto& nodeIndex : scene.nodes) {
			const auto& node = model.nodes[nodeIndex];
			parseDataFromNode(model, node);
		}
	}

	GltfInfo info;
	info.width = std::abs(minX - maxX);
	info.length = std::abs(minZ - maxZ);
	info.height = std::abs(minY - maxY);
	info.maxX = maxX;
	info.maxY = maxY;
	info.maxZ = maxZ;
	s_gltfInfoMap.insert(type, info);
};

/**
 *@brief recursive computation for model nodes
 *@note assume one node has maximum of one mesh, and one mesh has maximum of one primitive!
 */
void GLTFParser::parseDataFromNode(const tinygltf::Model& model, const tinygltf::Node& node, const glm::dmat4& parentTransform, const std::string& parentPath) {
	glm::dmat4 localTransform = glm::dmat4(1.0);// the local transformation matrix of the current node

	if (!node.matrix.empty())// if the node has a `matrix` attribute, use it directly
		localTransform = glm::make_mat4(node.matrix.data());
	else {// otherwise, combine translation, rotation, scale
		if (!node.translation.empty())
			localTransform = glm::translate(localTransform, glm::make_vec3(node.translation.data()));
		if (!node.rotation.empty()) {
			glm::dquat q = glm::make_quat(node.rotation.data());
			localTransform *= glm::mat4_cast(q);
		}
		if (!node.scale.empty())
			localTransform = glm::scale(localTransform, glm::make_vec3(node.scale.data()));
	}

	// the world transformation matrix of the current node = the world transformation matrix of the parent node * the local transformation matrix of the current node
	glm::dmat4 worldTransform = parentTransform * localTransform;
	s_transforms[type].insert(meshIndex, glm::mat4(worldTransform));

	std::string currentPath = parentPath.empty() ? node.name : parentPath + " -> " + node.name;

	if (node.mesh >= 0) { // node.mesh is an int, representing the associated mesh index
		const auto& mesh = model.meshes[node.mesh];
		/*if (type != ModelType::Zhiche) {
			std::string fullMeshName = currentPath + " -> " + mesh.name;
			std::cout << "Mesh Name: " << fullMeshName << std::endl;
			std::cout << "World Transform Matrix (Row-Major):\n";
			std::cout << std::fixed << std::setprecision(5);
			for (int i = 0; i < 4; ++i) {
				std::cout << "| ";
				for (int j = 0; j < 4; ++j) {
					std::cout << std::setw(10) << worldTransform[j][i] << " ";
				}
				std::cout << "|\n";
			}
			std::cout << "modelType = " << static_cast<int>(type) << " ----------------------------------------" << std::endl;
		}*/
		for (const auto& primitive : mesh.primitives) {// traverse each primitive(a geometric part of a mesh)
			// Vertex
			if (primitive.attributes.find("POSITION") != primitive.attributes.end()) {
				CVector<glm::vec3> _vertices;
				getAccessorData(_vertices, model, primitive.attributes.at("POSITION"));
				s_vertices[type].insert(meshIndex, _vertices);

				for (const auto& position : _vertices) {
					auto finalPos = glm::mat3(worldTransform) * position;
					minX = std::min(minX, finalPos.x);
					minY = std::min(minY, finalPos.y);
					minZ = std::min(minZ, finalPos.z);
					maxX = std::max(maxX, finalPos.x);
					maxY = std::max(maxY, finalPos.y);
					maxZ = std::max(maxZ, finalPos.z);
				}
			}

			// Index
			if (primitive.indices >= 0) {
				CVector<GLuint> _indices;
				getAccessorData<GLuint>(_indices, model, primitive.indices);
				s_indices[type].insert(meshIndex, _indices);
			}

			// Normal
			if (primitive.attributes.find("NORMAL") != primitive.attributes.end()) {
				CVector<glm::vec3> _normals;
				getAccessorData(_normals, model, primitive.attributes.at("NORMAL"));
				s_normals[type].insert(meshIndex, _normals);
			}

			// Texture Coord
			if (primitive.attributes.find("TEXCOORD_0") != primitive.attributes.end()) {
				CVector<glm::vec2> _texcoords;
				getAccessorData(_texcoords, model, primitive.attributes.at("TEXCOORD_0"));
				s_texcoords[type].insert(meshIndex, _texcoords);
			}

			// PbrMaterial
			if (primitive.material >= 0) {
				PbrMaterial _material;
				const auto& pbrMaterial = model.materials[primitive.material].pbrMetallicRoughness;
				const auto& baseColorFactor = pbrMaterial.baseColorFactor;// pure color
				if (baseColorFactor.size() == 4) {// #PbrMaterial (if size is not 4, it means bad datas in glTF file)
					float r = baseColorFactor[0], g = baseColorFactor[1], b = baseColorFactor[2], a = baseColorFactor[3];
					auto baseColor = CColor::fromRgbF(r, g, b, a);
					baseColor.eraseAlphaChannel();
					_material.baseColor = baseColor;
				}
				if (pbrMaterial.metallicFactor != -1)// [0, 1]
					_material.metallic = pbrMaterial.metallicFactor;
				else
					_material.metallic = 0;// no metallic

				if (pbrMaterial.roughnessFactor != -1)// [0, 1]
					_material.roughness = pbrMaterial.roughnessFactor;
				else
					_material.roughness = 0;// no roughness

				if (Config::Material_ForceUseCfgValue && ModelType::Zhiche != type) {
					if (ModelType::Cone == type || ModelType::Barrier == type)
						_material.baseColor = "Tomato";
					else
						_material.baseColor = Config::Material_BaseColor;
					_material.metallic = Config::Material_Metallic;
					_material.roughness = Config::Material_Roughness;
				}

				if (pbrMaterial.baseColorTexture.index >= 0) {// has texture image
					int textureIndex = pbrMaterial.baseColorTexture.index;
					const tinygltf::Texture& texture = model.textures[textureIndex];
					const tinygltf::Image& image = model.images[texture.source];
					CByteArray imgBytes = image.image;
					if (imgBytes.empty())
						LogEE << "texture image data is empty!";
					auto texImg = new OImage(imgBytes, image.width, image.height, image.component >= 4);
					_material.setTextureImage(texImg);
				}

				s_materials[type].insert(meshIndex, _material);
			}

			s_meshNumMap[type] = model.meshes.size();
			s_meshNames[type].insert(meshIndex, mesh.name);
			++meshIndex;
		}
	}

	// recursive processing of child nodes
	for (const auto& childIndex : node.children) {
		const auto& childNode = model.nodes[childIndex];
		// recursive computation of the world transformation matrix for child nodes
		parseDataFromNode(model, childNode, worldTransform, currentPath);
	}
};

void GLTFParser::startAsyncParsingZhiche(const std::function<void()>& completedCallback) {
	CUtil::doInBgThread([=] {
		parseAndTiming(ModelType::Zhiche, completedCallback);

		s_isZhicheParsedDone = true;
		const auto& info = s_gltfInfoMap[ModelType::Zhiche];

		DiskCache::updateVersion();
		DiskCache::updateZhicheInfo({info.width, info.length, info.height, info.maxX, info.maxY, info.maxZ});
	});
}

const GltfInfo& GLTFParser::zhicheInfo() {
	if (s_isZhicheParsedDone)
		return info(ModelType::Zhiche);

	const auto& zhicheInfo = DiskCache::getZhicheInfo();
	s_zhicheInfo.width = zhicheInfo.width;
	s_zhicheInfo.length = zhicheInfo.length;
	s_zhicheInfo.height = zhicheInfo.height;
	s_zhicheInfo.maxX = zhicheInfo.maxX;
	s_zhicheInfo.maxY = zhicheInfo.maxY;
	s_zhicheInfo.maxZ = zhicheInfo.maxZ;

	return s_zhicheInfo;
}

bool GLTFParser::isZhicheInfoAvailable() {
	if (s_isZhicheParsedDone || CUtil::fileExists(Config::View3D_ZhicheInfoPath))
		return s_zhicheInfo.isValid();
	return false;
}